/*
 *  sharedRegion.h
 *  kangmodb
 *
 *  Created by 강모 김 on 11. 5. 1..
 *  Copyright 2011 강모소프트. All rights reserved.
 *
 */

#ifndef _KD_SHARED_REGION_H_ 
#define _KD_SHARED_REGION_H_ (1)

#include "kdInfra.h"
#include "types.h"

// Implementation
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

/** @brief The header stored in a shared memory region. A shared memory region has shared chunks.
 *  The shared memory may or may not mapped to a file.
 */
typedef struct stgSharedRegionHeader
{
	// Stores the address that the shared memory region was mapped.
	// In case the system fails to map the shared memory region in this address,
	// we need to adjust all memory pointers in this region by the difference between the new mapped address and the original mapped address stored in this field.
	u64 mappedAddress;
	// The size of a shared memory chunk.
	u32 chunkSize;
	// The count of allocated shared memory chunks in this region.
	u32 allocatedChunkCount;
	// The maximum number of shared memory chunks that this shared region can have.
	u32 maxChunkCount;
	// List of free chunks in this region. 0 indicates no free chunk. Chunk ID starts from 1, increases monotonously.
	u32 freeChunkId;
} stgSharedRegionHeader;

/** @brief The NULL chunk Id
 */
const u32 STG_CHUNK_ID_NULL = KD_MAX_U32;

/** @brief The magic value for a free chunk.  
 */
const u64 STG_FREE_CHUNK_MAGIC = 0xcafebebe19760403;

/** @brief The header stored in a free memory chunk in the shared region.
 */
typedef struct stgFreeChunkHeader
{
	/** @brief A magic value for a free chunk. This is for assertion purpose.
	 */
	u64 freeChunkMagic;
	/** @brief Next free chunk ID.
	 */
	u32 nextFreeChunkId;
} stgFreeChunkHeader;

#include <gtest/gtest_prod.h>
class ClassSharedRegionTest;

/** @brief (Thread Safe) A shared memory region that keeps track of memory chunks. 
 * This region will be used for individual transaction log buffer, global log stream, and in-memory data structure for access methods.
 * It will be used for the following purposes.
 * a) Keep the key/value pair in shared memory to boost restart recovery.
 * b) Keep transaction log buffer in shared memory to quickly recover uncomitted data within key/value pair region.
 * c) Keep log stream in a mapped region to efficiently do disk I/O for committed log records.
 */ 
class stgSharedRegion
{
	FRIEND_TEST(ClassSharedRegionTest, PrivateMembers);
private :
	/** @brief The shared region has a header page containing all meta information.
	 *  After the header page, chunks come.
	 *  +-------------+----------+----------+-----+--------------+
	 *  | Header Page | Chunk #0 | Chunk #1 | ... | Chunk #(N-1) |
	 *  +-------------+----------+----------+-----+--------------+
	 */
	static const int HEADER_PAGE_SIZE_SHIFT_BITS = 13; // 2^13 = 8192
	static const int HEADER_PAGE_SIZE = 1<<HEADER_PAGE_SIZE_SHIFT_BITS;
	
	/** @brief Allocate a new chunk. 
	 * Throw KD_EXCP_REGION_NO_MORE_CHUNK if there is no more chunk.
	 */ 
	KD_VOID allocNewChunk( void ** chunkAddress )
	{
		KD_TRY
		{
			KD_DASSERT( map_ != NULL );
			stgSharedRegionHeader * regionHeader = (stgSharedRegionHeader*) map_;
			
			// Do we have chunks to allocate??
			if ( regionHeader->allocatedChunkCount < regionHeader->maxChunkCount )
			{
				// Allocate a new chunk.
				u32 newChunkId = regionHeader->allocatedChunkCount;
				
				*chunkAddress = calcChunkAddr( newChunkId );
				
				regionHeader->allocatedChunkCount ++;
			}
			else { // No more chunk!
				KD_THROW( KD_EXCP_REGION_NO_MORE_CHUNK );
			}
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}
	
	/** @brief See if this region has free chunks. 
	 */ 
	bool hasFreeChunks()
	{
		KD_DASSERT( map_ != NULL );

		return ((stgSharedRegionHeader*) map_)->freeChunkId != STG_CHUNK_ID_NULL ;
	}

	/** @brief See if a chunk is a free chunk. 
	 */ 
	bool isFreeChunk( const void * chunkAddress )
	{
		KD_DASSERT( chunkAddress != NULL );
		// If the magic value matches, it is a free chunk.
		return ((stgFreeChunkHeader*) chunkAddress)->freeChunkMagic  == STG_FREE_CHUNK_MAGIC;
	}

	/** @breif Make a chunk allocated. This is done by clearing the magic value for free chunks.
	 */
	static void makeAllocChunk( void * chunkAddress )
	{
		KD_DASSERT( chunkAddress != NULL );
		stgFreeChunkHeader * chunkHeader = (stgFreeChunkHeader *) chunkAddress;
		
		chunkHeader->freeChunkMagic = 0;
		chunkHeader->nextFreeChunkId = 0;
	}
	
	/** @breif Make a chunk free. This is done by setting the magic value for free chunks.
	 */
	static void makeFreeChunk( void * chunkAddress, u32 nextFreeChunkId )
	{
		KD_DASSERT( chunkAddress != NULL );
		stgFreeChunkHeader * chunkHeader = (stgFreeChunkHeader *) chunkAddress;
		
		chunkHeader->freeChunkMagic = STG_FREE_CHUNK_MAGIC;
		chunkHeader->nextFreeChunkId = nextFreeChunkId;
	}
	
	/** @brief Remove a free chunk from the free chunk list. 
	 */ 
	KD_VOID removeFreeChunk( void ** chunkAddress )
	{
		KD_DASSERT( chunkAddress != NULL );
		KD_DASSERT( map_ != NULL );
		stgSharedRegionHeader * regionHeader = (stgSharedRegionHeader *) map_;

		KD_TRY
		{
			// We should have a free chunk.
			KD_ASSERT( regionHeader->freeChunkId != STG_CHUNK_ID_NULL );
			void * firstFreeChunk = calcChunkAddr( regionHeader->freeChunkId );
			stgFreeChunkHeader * firstFreeChunkHeader = (stgFreeChunkHeader*) firstFreeChunk;
			KD_ASSERT( isFreeChunk(firstFreeChunk) );
			// Make the region header to point the next free chunk.
			regionHeader->freeChunkId = firstFreeChunkHeader->nextFreeChunkId;
			
			(void) makeAllocChunk( firstFreeChunk );
			
			*chunkAddress = firstFreeChunk;
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}

	/** @brief Add a free chunk to the free chunk list.
	 */ 
	KD_VOID addFreeChunk( void * chunkAddress )
	{
		KD_DASSERT( chunkAddress != NULL );
		KD_DASSERT( map_ != NULL );
		stgSharedRegionHeader * regionHeader = (stgSharedRegionHeader *) map_;
	
		KD_TRY
		{
			(void)makeFreeChunk( chunkAddress, regionHeader->freeChunkId );

			regionHeader->freeChunkId = calcChunkId( chunkAddress );
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}

	/** @brief Calculate the address of a chunk by chunkId. chunkId starts from 0, increases monotonously.
  	 */
	KD_INLINE void * calcChunkAddr(u32 chunkId)
	{
		KD_DASSERT( map_ != NULL );
		KD_DASSERT( chunkSizeShiftBits_ > 0 );
		
		// Optimization : instead of multiplying chunk size to chunkId, let's use bitwise shift.
		uptr chunkOffset = (chunkId << chunkSizeShiftBits_);
		return (char*) map_ + HEADER_PAGE_SIZE + chunkOffset;
	}

	/** @brief Calculate the chunk ID of a chunk by the address of it. chunkId starts from 0, increases monotonously.
  	 */
	KD_INLINE u32 calcChunkId(void * chunkAddr)
	{
		KD_DASSERT( map_ != NULL );
		KD_DASSERT( chunkAddr != NULL );
		KD_DASSERT( chunkSizeShiftBits_ > 0 );
		
		uptr chunkOffset = (char*) chunkAddr - ((char*) map_ + HEADER_PAGE_SIZE);
		
		return (chunkOffset >> chunkSizeShiftBits_);
	}


	/** @brief Calculate the size of mapped file for a region with given chunk size and count.
	*/
	static KD_INLINE filesize_t calcFileSize(u32 sharedChunkSize, u32 maxChunkCount )
	{
		// Calculate the file size. It should be able to keep a header page and chunks and a byte for stretching the file.
		filesize_t expectedFileSize = HEADER_PAGE_SIZE + sharedChunkSize * maxChunkCount + 1 ;
		
		return expectedFileSize;
	}

	/** @brief Calculate the size of mapped file for a region with given chunk size and count.
	 *         If the region file exists, open the existing one. Otherwise, create a new one. 
     *
	 * @param regionName      The file name that will be mapped to a memory address.
	 * @param sharedChunkSize The size of a shared chunk.
	 * @param maxChunkCount   The maximum chunk count.
	*/
	KD_VOID openRegionFile(const KD_STRING & regionName, u32 sharedChunkSize, u32 maxChunkCount, bool* fileCreated )
	{
		bool isFileCreated = false;
		
		const char * regionNameStr = regionName.c_str();
		
		KD_TRY
		{
			// The file should not be open yet.
			KD_DASSERT( fd_ == 0 );
			
			
			// Calculate the file size. It should be able to keep a header page and chunks.
			filesize_t expectedFileSize = calcFileSize( sharedChunkSize, maxChunkCount) ;

			// The file exists
			if ( access( regionNameStr, F_OK) == 0 )
			{
				// Open the file.
				fd_ = open( regionNameStr, O_RDWR );
				if ( fd_ == -1 )
				{
					KD_THROW( KD_EXCP_OPEN_REGION_FILE );
				}
				
				// Check the file size. 
				filesize_t actualFileSize = lseek( fd_, 0, SEEK_END);
				
				if ( actualFileSize != expectedFileSize )
				{
					KD_THROW( KD_EXCP_INVALID_REGION_FILE );
				}
			} 
			else // The file does not exist. Create a new one.
			{
				fd_ = open( regionNameStr, O_RDWR | O_CREAT | O_TRUNC, (mode_t) 0600 );
				if ( fd_ == -1 )
				{
					KD_THROW( KD_EXCP_CREATE_REGION_FILE );
				}
				
				isFileCreated = true;
				
				// Offset from 0 to (fileSize -1) will be used by the shared memory region.
				// So we stretch the file to fileSize.
				if ( lseek( fd_, expectedFileSize-1, SEEK_SET) == -1 )
				{
					KD_THROW( KD_EXCP_STRETCH_REGION_FILE );
				}
				
				// Write '\0' to the end of the file. ("" points to a character array whose first element is '\0').
				if ( write( fd_, "", 1) != 1 )
				{
					KD_THROW( KD_EXCP_STRETCH_REGION_FILE );
				}
			}
			
			// Set the output parameter indicating if the region file was created.
			*fileCreated = isFileCreated;
		}
		KD_CATCH
		{
			if ( isFileCreated )
			{
				KD_ASSERT( unlink( regionNameStr ) == 0 );
			}
		}
		KD_FINALLY
		KD_END
	}
	
	/** @brief Close the region file.
	 * Throw KD_EXCP_CLOSE_REGION_FILE if it fails.
	 */
	KD_VOID closeRegionFile()
	{
		KD_TRY
		{
			if ( close(fd_) == -1 )
			{
				KD_THROW( KD_EXCP_CLOSE_REGION_FILE );
			}
			fd_ = 0;
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}

	
	/** @brief The file where the memory is mapped.
	 */
	int fd_ ;

	/** @brief The address of the memory mapped area. 
	 */
	void * map_;
	
	/** @brief The number of bitwise shifts for calculating a chunk size. This is used for calculating chunk offset by chunk Id 
	 */
	u32 chunkSizeShiftBits_;
	
	/** @brief The expected size of mapped file for the shared region. Calculated from chunk size and maximum chunk count. 
	*/
	filesize_t expectedFileSize_;
	
	/** @brief The next chunk Id to return for an iteration.
	*/
	u32 nextChunkId_;
public :
	stgSharedRegion()
	{
		fd_ = 0;
		map_ = NULL;
		expectedFileSize_ = 0;
		chunkSizeShiftBits_ = 0;
		nextChunkId_ = STG_CHUNK_ID_NULL;
	}
	~stgSharedRegion()
	{
	}
	
	/** @brief Open a shared memory region with a regionName.
	 *         If the region exists, open the existing one. Otherwise, create a new one. 
	 *		   The mappedAddr is the address of mapped memory to a file. 
	 * 
	 * @param regionName      The file name that will be mapped to a memory address.
	 * @param mappedAddr      The address to map the file. If this is NULL, map the file to any address that OS suggests. Otherwise, map the file to the given address.
	 * @param sharedChunkSize The size of a shared chunk.
	 * @param maxChunkCount   The maximum chunk count.
	 */
	KD_VOID openRegion(const KD_STRING & regionName, void * mappedAddr, u32 sharedChunkSize, u32 maxChunkCount )
	{
		bool isFileCreated = false;
		
		const char * regionNameStr = regionName.c_str();
		
		KD_TRY
		{
		
			KD_ASSERT( kdMemIsAlignedBy8Byte((uptr)mappedAddr) );
			KD_ASSERT( kdMemIsAlignedBy8Byte((uptr)sharedChunkSize) );
			
			chunkSizeShiftBits_ = kdMemGetAlignedBits(sharedChunkSize);
		
			// Calculate the file size. It should be able to keep a header page and chunks.
			expectedFileSize_ = calcFileSize( sharedChunkSize, maxChunkCount) ;

			KD_CALL( openRegionFile(regionName, sharedChunkSize, maxChunkCount, &isFileCreated ) );
			
			if ( mappedAddr )
			{
				// Map the file to the given address.
				map_ = mmap( mappedAddr, expectedFileSize_, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd_, 0 /* offset */);
				if ( map_ == MAP_FAILED )
				{
//					printf("error : %s\n", strerror(errno));
					KD_THROW( KD_EXCP_MAP_REGION_FILE );
				}
				// Make sure the mapped address equals to the proposed address.
				KD_ASSERT( map_ ==  mappedAddr );
			}
			else 
			{
				// We can't map an existing file to an arbitary address, because the existing file can store memory pointers based on the previously mapped address.
				// So we should be creating a new region file to map the file to an arbitary address.
				KD_ASSERT( isFileCreated );

				// Map the file to an arbitary address.
				map_ = mmap( 0, expectedFileSize_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0 /* offset */);
				if ( map_ == MAP_FAILED )
					KD_THROW( KD_EXCP_MAP_REGION_FILE );
			}
			
			// IF the region file is newly created, initialize the region header.
			stgSharedRegionHeader * regionHeader = (stgSharedRegionHeader*) map_;
			if ( isFileCreated )
			{
				regionHeader->mappedAddress = (uptr)map_;
				regionHeader->chunkSize = sharedChunkSize;
				regionHeader->allocatedChunkCount = 0;
				regionHeader->maxChunkCount = maxChunkCount;
				regionHeader->freeChunkId = STG_CHUNK_ID_NULL;
			}
			else { // The file was opened.
				// But it is mapped to a different address.
				if ( regionHeader->mappedAddress != (uptr)map_ )
				{
					KD_THROW( KD_EXCP_DIFFERNT_MAP_ADDR );
				}
			}

		}
		KD_CATCH
		{
			if ( isFileCreated )
			{
				KD_ASSERT( unlink( regionNameStr ) == 0 );
			}
		}
		KD_FINALLY
		KD_END
	}
	
	void * getMappedAddress()
	{
		KD_DASSERT( map_ != NULL );
		stgSharedRegionHeader * regionHeader = (stgSharedRegionHeader *) map_;
		return (void*)regionHeader->mappedAddress;
	}
	
	/** @brief Remove the shared memory region if it exists. The Region should not be opened yet. Otherwise, it hits an assertion.
	 */
	KD_VOID removeRegionFile(const KD_STRING & regionName )
	{
		const char * regionNameStr = regionName.c_str();
	
		KD_ASSERT( map_ == NULL );
		KD_ASSERT( fd_ == 0 );
		
		KD_TRY
		{
			
			// The file exists.
			if ( access( regionNameStr, F_OK) == 0 )
			{
				if ( unlink( regionNameStr) != 0 )
				{
					KD_THROW( KD_EXCP_DELETE_REGION_FILE );
				}
			}
			else // The file does not exist. 
			{
				// Do nothing.
			}
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}

	// TODO : Add Test Case.
	/** @brief Get the address and available size for the custom header.
	 *  Custom header can be used by different modules that want to keep their own meta information.
	 *
	 *  The header page starts with the stgSharedRegionHeader structure, and then the custom header follows.
	 *  - The stgSharedRegionHeader keeps common meta information used by all modules.
	 *  - The Custome Header Area keeps the meta information specific to a module.
	 *  - The total size is stgSharedRegion::HEADER_PAGE_SIZE, which is 8192 bytes currently.
     *  +-----------------------+---------------------+
	 *  | stgSharedRegionHeader | Custom Header Area  |
     *  +-----------------------+---------------------+
	 */
	KD_VOID getCustomHeader( void ** customHeader, int * customHeaderSize )
	{
		KD_TRY
		{
			KD_DASSERT( map_ != NULL );
			*customHeaderSize = HEADER_PAGE_SIZE - sizeof(stgSharedRegionHeader);
			*customHeader = (char*)map_ + sizeof(stgSharedRegionHeader);
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}

	/** @brief Get a chunk from this region 
	 * Throw KD_EXCP_REGION_NO_MORE_CHUNK if there is no more chunk.
	 */
	KD_VOID checkout( void ** chunkAddress )
	{
		KD_ASSERT( chunkAddress != NULL );
		
		KD_TRY
		{
			// TODO : Thread Safety
			if ( hasFreeChunks() )
			{
				KD_CALL( removeFreeChunk( chunkAddress ) );
			}
			else 
			{
				KD_CALL( allocNewChunk( chunkAddress ) );
			}

		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}

	/** @brief Return a chunk to this region. 
	 */
	KD_VOID checkin( void * chunkAddress)
	{
		KD_ASSERT( chunkAddress != NULL );
		
		KD_TRY
		{
			// TODO : Thread Safety
			KD_CALL( addFreeChunk( chunkAddress ) );
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}

	/** @brief Initialize chunk iteration. Skip all free chunks, but iterate chunks in use by calling checkout function.
	 * Upon restart recovery, KangmoDB iterates all used chunks in the shared memory area 
	 * to reconstruct stgTable objects which have chunks allocated from the shared region.
	 * 
	 * No need to gaurantee Thread Safety, because this function is called during the restart recovery only.
	 */
	KD_VOID initChunkIteration()
	{
		KD_TRY
		{
			
			nextChunkId_ = 0;
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}

	/** @brief Iterate a chunk for each time this function is called. Skip all free chunks, but iterate chunks in use by calling checkout function.
	 *  At the end of the iteration, *chunkAddress points to NULL.
	 *
	 * No need to gaurantee Thread Safety, because this function is called during the restart recovery only.
	 */
	KD_VOID iterateChunk(void ** chunkAddress)
	{
		KD_DASSERT( map_ != NULL );
		
		stgSharedRegionHeader * regionHeader = (stgSharedRegionHeader *) map_;
		
		KD_TRY
		{
			for( ;nextChunkId_ < regionHeader->maxChunkCount; nextChunkId_ ++ )
			{
				void * chunkAddr = calcChunkAddr( nextChunkId_ );
				
				// Skip free chunks.
				if ( isFreeChunk( chunkAddr ) )
					continue;
				
				// Ok, we found a chunk to iterate.
				*chunkAddress = chunkAddr;
				nextChunkId_++;
				KD_RETURN;
			}
			// No chunk found;
			*chunkAddress = NULL;
			nextChunkId_ = STG_CHUNK_ID_NULL;
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}

	/** @brief Unmap the mapped file. 
	 */
	KD_VOID closeRegion()
	{
		KD_TRY
		{
			// TODO : Thread Safety
			if ( map_ == NULL || fd_ == 0 )
			{
				KD_THROW( KD_EXCP_REGION_NOT_OPEN );
			}
			
			if ( munmap( map_, expectedFileSize_ ) == -1 )
			{
				KD_THROW( KD_EXCP_UNMAP_REGION_FILE );
			}
			
			KD_CALL( closeRegionFile() );
			
			fd_ = 0;
			map_ = NULL;
			expectedFileSize_ = 0;
			chunkSizeShiftBits_ = 0;
			nextChunkId_ = STG_CHUNK_ID_NULL;
		}
		KD_CATCH
		KD_FINALLY
		KD_END
	}
};


#endif /* _KD_SHARED_REGION_H_ */